IServiceProvider
HttpClientFactory,HTTP 呼叫更穩定為了強調「資料層門面」的角色,我們把介面統一為 IDatabaseRepository。
實作細節(LiteDB)今天不展開;你只要知道 ViewModel 只依賴 IDatabaseRepository,容器會提供實作。
在 WPF 專案 MyStockApp 安裝以下 NuGet:
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Http
<!-- MyStockApp/App.xaml -->
<Application x:Class="MyStockApp.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <!-- 不指定 StartupUri,改由 App.xaml.cs 控制 -->
</Application>
// MyStockApp/App.xaml.cs
using System;
using System.Windows;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System.Net.Http;
namespace MyStockApp
{
    public partial class App : Application
    {
        private IHost _host;
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
            _host = Host.CreateDefaultBuilder()
                .ConfigureServices((context, services) =>
                {
                    // 1) DB 門面
                    services.AddSingleton<IDatabaseRepository, LiteDbStockRepository>(sp =>
                        new LiteDbStockRepository("StockData.db")); // 路徑可抽設定
                    // 2) HttpClientFactory + API 服務(Typed Client)
                    services.AddHttpClient<IStockApiService, TwseStockApiService>(client =>
                    {
                        // client.BaseAddress = new Uri("https://openapi.twse.com.tw/");
                        // 可設定逾時、Header 等
                        client.Timeout = TimeSpan.FromSeconds(30);
                    });
                    // 3) 資料抓取 / 指標計算 / 批次工作
                    services.AddSingleton<IQuoteFetchService, QuoteFetchService>();
                    services.AddSingleton<IIndicatorService, IndicatorService>();
                    services.AddSingleton<IBuildIndicatorsJob, BuildIndicatorsJob>();
                    // 4) ViewModel 與 View
                    services.AddTransient<StockFilterViewModel>();  // 每次新建一個 VM
                    services.AddTransient<MainWindow>();            // 由容器建立並注入 VM
                })
                .Build();
            // 從容器取得 MainWindow(其依賴會自動注入)
            var main = _host.Services.GetRequiredService<MainWindow>();
            main.Show();
        }
        protected override async void OnExit(ExitEventArgs e)
        {
            if (_host is not null) await _host.StopAsync();
            _host?.Dispose();
            base.OnExit(e);
        }
    }
}
GetDailyQuotes(code, start, end) 從 ICrawler 移過來)以上在 Day 24 已完成;今天只是把它們註冊進 DI。
// MyStockApp/MainWindow.xaml.cs
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
namespace MyStockApp
{
    public partial class MainWindow : Window
    {
        public MainWindow(StockFilterViewModel vm)
        {
            InitializeComponent();
            DataContext = vm; // 由 DI 傳入,解除 new 依賴
        }
    }
}
<!-- MyStockApp/MainWindow.xaml -->
<Window x:Class="MyStockApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MyStockApp" Width="900" Height="600">
    <DockPanel>
        <!-- 你可以把 Day 23 的樣式 / Day 24 的表格延續使用 -->
        <ContentControl Content="{Binding}">
            <!-- 綁定的是 StockFilterViewModel,DataContext 已在 code-behind 設好 -->
        </ContentControl>
    </DockPanel>
</Window>
重點:
- 依賴 IDatabaseRepository(容器注入)
- 提供「載入股票」「重整指標」「清除條件」命令
- 篩選:關鍵字 / 指定日期的 KD 黃金交叉 / MACD 柱正
- 以快取
_indicatorCache減少資料庫打點
// MyStockApp/ViewModel/StockFilterViewModel.cs
using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Data;
using System.Windows.Input;
namespace MyStockApp
{
    public class StockFilterViewModel : INotifyPropertyChanged
    {
        private readonly IDatabaseRepository _repo;
        public StockFilterViewModel(IDatabaseRepository repo)
        {
            _repo = repo ?? throw new ArgumentNullException(nameof(repo));
            View = CollectionViewSource.GetDefaultView(Stocks);
            View.Filter = FilterPredicate;
            _selectedDate = DateTime.Now.Date;
            LoadFromLocalCommand = new AsyncCommand(LoadFromLocalAsync);
            RefreshIndicatorsCommand = new AsyncCommand(RefreshIndicatorCacheAsync, () => Stocks.Count > 0);
            ClearFiltersCommand = new RelayCommand(_ =>
            {
                Query = string.Empty;
                OnlyKdGoldenCross = false;
                OnlyMacdPositive = false;
            });
        }
        // ===== 資料源 =====
        public ObservableCollection<StockProfile> Stocks { get; } = new();
        public ICollectionView View { get; }
        // ===== 條件 =====
        private string _query = string.Empty;
        public string Query
        {
            get => _query;
            set { _query = value ?? string.Empty; OnPropertyChanged(nameof(Query)); View.Refresh(); }
        }
        private DateTime _selectedDate;
        public DateTime SelectedDate
        {
            get => _selectedDate;
            set { _selectedDate = value.Date; OnPropertyChanged(nameof(SelectedDate)); _ = RefreshIndicatorCacheAsync(); }
        }
        private bool _onlyKdGoldenCross;
        public bool OnlyKdGoldenCross
        {
            get => _onlyKdGoldenCross;
            set { _onlyKdGoldenCross = value; OnPropertyChanged(nameof(OnlyKdGoldenCross)); View.Refresh(); }
        }
        private bool _onlyMacdPositive;
        public bool OnlyMacdPositive
        {
            get => _onlyMacdPositive;
            set { _onlyMacdPositive = value; OnPropertyChanged(nameof(OnlyMacdPositive)); View.Refresh(); }
        }
        // ===== 指令 =====
        public ICommand LoadFromLocalCommand { get; }
        public ICommand RefreshIndicatorsCommand { get; }
        public ICommand ClearFiltersCommand { get; }
        // ===== 指標快取(指定日)=====
        private readonly Dictionary<string, DailyIndicator?> _indicatorCache = new();
        private async Task LoadFromLocalAsync()
        {
            Stocks.Clear();
            var profiles = await _repo.QueryProfilesAsync(); // 由資料層提供主檔清單
            foreach (var p in profiles) Stocks.Add(p);
            await RefreshIndicatorCacheAsync();
        }
        private async Task RefreshIndicatorCacheAsync()
        {
            _indicatorCache.Clear();
            foreach (var s in Stocks)
            {
                var ind = await _repo.GetIndicatorAtOrBeforeAsync(s.Code, SelectedDate);
                _indicatorCache[s.Code] = ind;
            }
            (RefreshIndicatorsCommand as AsyncCommand)?.RaiseCanExecuteChanged();
            View.Refresh();
        }
        private bool FilterPredicate(object obj)
        {
            if (obj is not StockProfile s) return false;
            if (!string.IsNullOrWhiteSpace(Query))
            {
                var q = Query.Trim();
                if (!s.Code.Contains(q, StringComparison.OrdinalIgnoreCase) &&
                    !s.Name.Contains(q, StringComparison.OrdinalIgnoreCase))
                    return false;
            }
            if (OnlyKdGoldenCross || OnlyMacdPositive)
            {
                if (!_indicatorCache.TryGetValue(s.Code, out var ind) || ind is null) return false;
                if (OnlyKdGoldenCross && !(ind.K.HasValue && ind.D.HasValue && ind.K.Value > ind.D.Value))
                    return false;
                if (OnlyMacdPositive && !(ind.MACDHist.HasValue && ind.MACDHist.Value > 0m))
                    return false;
            }
            return true;
        }
        public event PropertyChangedEventHandler? PropertyChanged;
        protected void OnPropertyChanged(string name)
            => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}
上例使用的
StockProfile、DailyIndicator、AsyncCommand、RelayCommand請沿用專案內既有定義。
如果IDatabaseRepository方法名稱不同(例如GetIndicatorAtOrBeforeAsync),請對照你的介面做小調整即可。
IDatabaseRepository、IndicatorService、QuoteFetchService、BuildIndicatorsJob → Singleton
StockFilterViewModel、MainWindow → Transient
IStockApiService → AddHttpClient(背後用 HttpClientFactory)